Skip to content

backends: name→package registry — decouple the name layer from the closed enum (#386 Phase 5) - #397

Merged
apotema merged 3 commits into
mainfrom
worktree-agent-a258ce70c51247675
Jun 28, 2026
Merged

backends: name→package registry — decouple the name layer from the closed enum (#386 Phase 5)#397
apotema merged 3 commits into
mainfrom
worktree-agent-a258ce70c51247675

Conversation

@apotema

@apotema apotema commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

The name→package registry — the Phase-5 pluggability seam. Centralizes every scattered @tagName(cfg.backend)-based package-layout derivation into one string-keyed module, so the splice/codegen no longer reach the closed Backend enum for a backend's identity.

What

  • src/backend_registry.ziglookup(allocator, name) → BackendInfo { name, subpath "backends/{name}", zon_name "labelle_{name}", link_name "labelle-{name}" }, keyed by string (resolves names that are NOT enum tags — the point). builtin_names is comptime-derived from config.Backend so registry/enum can't drift. + isBuiltin, free.
  • config.backendName() — the seam future code reads (returns @tagName(self.backend) for now; .backend parsing unchanged).
  • Routed every name site through the registry: deps_linker, build_files:745, root (target dir + loadBackendTemplate), main:217, gui_resolve diagnostics, and the manifest-splice's backendPackageDir locator (the seam flagged in bgfx: manifest-driven codegen-splice for bgfx-desktop (epic #386 Phase 3/5) #396).
  • Left the behavioral switch(cfg.backend) sites (gamepad sub-packages, codegen fragments) — those select backend-specific behavior, the manifest-splice's job, not the name layer.

Result

Zero @tagName(cfg.backend) remain in code (only comments). The only thing still coupling a backend to the enum is config parsing (.backend is the closed enum) + the behavioral switches — exactly what the next step (open the resolver) tackles.

Verified

Part of epic #386 Phase 5. Next: open config to parse an arbitrary backend name+package (the enum becomes a fast-path shorthand).

Summary by CodeRabbit

  • New Features

    • Introduced a centralized backend naming/metadata lookup to support pluggable backends beyond the built-in set.
    • Generated output paths now consistently use the selected backend’s canonical name.
  • Bug Fixes

    • Improved backend package resolution across dependency linking, manifest loading/splicing, and backend template handling to reduce mismatched paths.
    • Updated GUI validation diagnostics and errors to display consistent backend names.
  • Tests

    • Added coverage to ensure backend naming conventions stay in sync and work for both built-in and non-enum backend names.

apotema added 2 commits June 28, 2026 14:22
#386 Phase 5)

Introduce src/backend_registry.zig — a string-keyed registry that derives a
backend's package facts (subpath backends/{name}, zon_name labelle_{name},
link_name labelle-{name}) from a plain name string instead of the closed
config.Backend enum tag. This is the pluggability seam: lookup() resolves a
name that has NO enum tag, so a future resolver can hand the name layer a
third-party backend name.

Add config.ProjectConfig.backendName() as the seam future code reads instead
of @TagName(cfg.backend). The enum stays the backward-compat shorthand; parsing
arbitrary names is the explicit follow-up.

Route the ~8 name-derivation sites through the registry / backendName():
deps_linker (backend dep entry), build_files (build.zig.zon backend dep path),
root.zig (backend template subpath + target dir name), main.zig (target dir
name), gui_resolve.zig (3 diagnostics). Behavioral switch(cfg.backend) sites
(codegen selection, gamepad sub-package staging) are intentionally left alone —
those are the manifest splice's job, not the name layer.

Tests: pluggability (fictional backend resolves), drift guard (builtin_names
<-> Backend tags agree both ways), and a per-built-in inline-convention match.
Byte-identical verified: bgfx-desktop + raylib-desktop build.zig / build.zig.zon
/ game.zig + target dir names unchanged before/after.
…hase-5 seam locator)

The manifest-splice locator was the last @TagName(cfg.backend) in code (the
registry PR's original base predated manifest_splice.zig, so the agent couldn't
reach it). Now routed through backend_registry.lookup(cfg.backendName()) — the
only residual enum coupling is config PARSING (.backend is still the enum).
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d3bf7cb5-1b6d-40ab-bfc0-ff3f2a93b144

📥 Commits

Reviewing files that changed from the base of the PR and between 9748d87 and 44bdf7f.

📒 Files selected for processing (2)
  • src/backend_registry.zig
  • src/deps_linker.zig
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/deps_linker.zig
  • src/backend_registry.zig

📝 Walkthrough

Walkthrough

Adds a backend registry module for deriving backend package names and paths, introduces a ProjectConfig.backendName() accessor, and updates backend-related path/name generation sites to use the new registry-based seam.

Changes

Backend Registry Introduction and Migration

Layer / File(s) Summary
Backend registry API
src/backend_registry.zig
Defines BackendInfo ownership, lookup()/free() for derived backend strings, and builtin_names/isBuiltin() from config.Backend tags.
Registry tests and drift guard
src/backend_registry.zig
Tests verify built-in derivation, non-enum pluggability, builtin-name drift checks, and formatting consistency across all built-ins.
ProjectConfig.backendName() seam
src/config.zig
Adds backendName() as a named accessor returning the backend enum tag string.
Backend name and package resolution updates
src/build_files.zig, src/deps_linker.zig, src/codegen/manifest_splice.zig, src/root.zig, src/main.zig, src/gui_resolve.zig
Call sites switch to cfg.backendName() and registry lookups when deriving backend names, subpaths, and bundled package locations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped through names both tidy and new,
A registry path kept the strings true.
No more ad-hoc tags in the moonlit night,
Backends now find their way just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: adding a backend name→package registry to decouple naming from the enum.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-agent-a258ce70c51247675

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a centralized backend_registry module to decouple backend package-layout conventions from a closed enum, routing lookups through string names instead. The review feedback identifies critical memory leak vulnerabilities in src/backend_registry.zig and src/deps_linker.zig during allocation failures, recommending the use of errdefer for proper resource cleanup. Additionally, it suggests replacing std.debug.print with standard logging facilities (std.log.err) for user-facing errors in src/gui_resolve.zig to align with CLI best practices.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/backend_registry.zig
Comment on lines +53 to +60
pub fn lookup(allocator: std.mem.Allocator, name: []const u8) !BackendInfo {
return .{
.name = name,
.subpath = try std.fmt.allocPrint(allocator, "backends/{s}", .{name}),
.zon_name = try std.fmt.allocPrint(allocator, "labelle_{s}", .{name}),
.link_name = try std.fmt.allocPrint(allocator, "labelle-{s}", .{name}),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If any of the subsequent std.fmt.allocPrint calls fail (e.g., for zon_name or link_name), the previously allocated fields (like subpath) will be leaked. Use errdefer to clean up partially allocated fields on failure.

pub fn lookup(allocator: std.mem.Allocator, name: []const u8) !BackendInfo {
    const subpath = try std.fmt.allocPrint(allocator, "backends/{s}", .{name});
    errdefer allocator.free(subpath);
    const zon_name = try std.fmt.allocPrint(allocator, "labelle_{s}", .{name});
    errdefer allocator.free(zon_name);
    const link_name = try std.fmt.allocPrint(allocator, "labelle-{s}", .{name});
    return .{
        .name = name,
        .subpath = subpath,
        .zon_name = zon_name,
        .link_name = link_name,
    };
}

Comment thread src/deps_linker.zig Outdated
Comment on lines +55 to +60
const backend_info = try backend_registry.lookup(allocator, cfg.backendName());
defer allocator.free(backend_info.subpath);
const backend_path = try cache.resolveBundledPackage(allocator, cfg.labelle_version, cfg.assembler_version, project_dir, backend_info.subpath);
// zon_name / link_name are moved into the DepEntry (freed by
// freeDepEntries), so we don't free them here.
try deps.append(allocator, .{ .zon_name = backend_info.zon_name, .link_name = backend_info.link_name, .abs_path = backend_path });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If cache.resolveBundledPackage or deps.append fails, backend_info.zon_name and backend_info.link_name will be leaked because they are not freed by defer allocator.free(backend_info.subpath). Additionally, backend_path will leak if deps.append fails. Use errdefer to ensure these resources are freed on failure.

        const backend_info = try backend_registry.lookup(allocator, cfg.backendName());
        defer allocator.free(backend_info.subpath);
        errdefer allocator.free(backend_info.zon_name);
        errdefer allocator.free(backend_info.link_name);
        const backend_path = try cache.resolveBundledPackage(allocator, cfg.labelle_version, cfg.assembler_version, project_dir, backend_info.subpath);
        errdefer allocator.free(backend_path);
        // zon_name / link_name are moved into the DepEntry (freed by
        // freeDepEntries), so we don't free them here.
        try deps.append(allocator, .{ .zon_name = backend_info.zon_name, .link_name = backend_info.link_name, .abs_path = backend_path });

Comment thread src/gui_resolve.zig

const bridge_def = getBridgeForBackend(bridges, cfg.backend) orelse {
std.debug.print("labelle: GUI plugin '{s}' requires a bridge for backend '{s}', but none is declared in gui.labelle.\n", .{ manifest.name, @tagName(cfg.backend) });
std.debug.print("labelle: GUI plugin '{s}' requires a bridge for backend '{s}', but none is declared in gui.labelle.\n", .{ manifest.name, cfg.backendName() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using std.debug.print for user-facing errors in CLI tools. Use standard logging facilities like std.log.err instead.

            std.log.err("labelle: GUI plugin '{s}' requires a bridge for backend '{s}', but none is declared in gui.labelle.", .{ manifest.name, cfg.backendName() });
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

Comment thread src/gui_resolve.zig
bridge_dir = try std.fs.path.resolve(allocator, &.{ plugin_dir, rel_path });
} else {
std.debug.print("labelle: GUI plugin '{s}' bridge for '{s}' has no .path (remote bridge resolution not yet supported)\n", .{ manifest.name, @tagName(cfg.backend) });
std.debug.print("labelle: GUI plugin '{s}' bridge for '{s}' has no .path (remote bridge resolution not yet supported)\n", .{ manifest.name, cfg.backendName() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using std.debug.print for user-facing errors in CLI tools. Use standard logging facilities like std.log.err instead.

            std.log.err("labelle: GUI plugin '{s}' bridge for '{s}' has no .path (remote bridge resolution not yet supported)", .{ manifest.name, cfg.backendName() });
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

Comment thread src/gui_resolve.zig

if (bridge_def.adapter.len == 0) {
std.debug.print("labelle: GUI plugin '{s}' bridge for '{s}' has empty .adapter name\n", .{ manifest.name, @tagName(cfg.backend) });
std.debug.print("labelle: GUI plugin '{s}' bridge for '{s}' has empty .adapter name\n", .{ manifest.name, cfg.backendName() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using std.debug.print for user-facing errors in CLI tools. Use standard logging facilities like std.log.err instead.

            std.log.err("labelle: GUI plugin '{s}' bridge for '{s}' has empty .adapter name", .{ manifest.name, cfg.backendName() });
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend_registry.zig`:
- Around line 53-59: The BackendRegistry.lookup allocation path can leak memory
if a later allocPrint fails because earlier strings are never freed before the
function returns an error. Update lookup to build BackendInfo incrementally
using the same allocator, and add errdefer cleanup for each successfully
allocated field (subpath, zon_name, link_name) so partial allocations are
released on failure. Use the lookup function and the BackendInfo fields as the
places to apply the fix.

In `@src/deps_linker.zig`:
- Around line 55-60: The fallback path in `deps_linker.zig` is leaking
allocations if `cache.resolveBundledPackage` or `deps.append` fails. In the
`backend_registry.lookup` flow, add cleanup for `backend_info.zon_name`,
`backend_info.link_name`, and `backend_path` before the dep is appended, while
keeping the ownership transfer to `DepEntry` only on success; use the existing
`backend_info`/`deps.append` block to ensure all allocated values are freed on
error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 064958ab-d236-42a8-b12a-9a8f5cc44204

📥 Commits

Reviewing files that changed from the base of the PR and between 0422169 and 9748d87.

📒 Files selected for processing (8)
  • src/backend_registry.zig
  • src/build_files.zig
  • src/codegen/manifest_splice.zig
  • src/config.zig
  • src/deps_linker.zig
  • src/gui_resolve.zig
  • src/main.zig
  • src/root.zig

Comment thread src/backend_registry.zig Outdated
Comment thread src/deps_linker.zig Outdated
- backend_registry.lookup: allocate subpath/zon_name/link_name incrementally with
  errdefer so a mid-sequence OOM doesn't leak the already-allocated fields (the
  struct never returns → caller never frees).
- deps_linker: tight nested scope with errdefer for the moved-into-DepEntry fields
  (zon_name/link_name/backend_path) — frees them on any error up to the append, but
  NOT after (ownership → deps), and scoped so the later gamepad appends can't
  re-trigger a double-free.
Both Gemini + CodeRabbit. (gui_resolve debug.print are pre-existing — the PR only
swapped the name arg; left for a separate file-wide log cleanup.)
@apotema

apotema commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the two leak findings (backend_registry.lookup + deps_linker) with errdefer — both real, both PR-introduced. The deps_linker one needed a tight nested scope so the errdefers free the moved-into-DepEntry fields on error up to the append but don't double-free when the later gamepad appends fail.

The 3 gui_resolve.zig std.debug.print findings (50/61/66) are pre-existing — this PR only swapped the argument (@tagName(cfg.backend)cfg.backendName()) on those lines; it didn't introduce the prints. The file has ~7 such prints, and line 50 is a multi-part streamed diagnostic (message → bridge-list loop → newline) that converting piecemeal would garble. Converting 3-of-7 mid-file mixes a logging cleanup into a name-layer refactor, so I've left them for a separate file-wide debug.printlog.err pass. Flagging here for visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant